/*******************************************************************************
 * Copyright (c) 2001, 2002 Boris Pruessmann and others. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Common Public License v0.5 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/cpl-v05.html
 * 
 * Contributors: Boris Pruessmann - Initial implementation.
 ******************************************************************************/
package net.sourceforge.perforce.ui;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.internal.P4TeamProvider;
import net.sourceforge.perforce.core.resources.LocalFile;
import net.sourceforge.perforce.core.resources.LocalFolder;
import net.sourceforge.perforce.core.resources.LocalResource;
import net.sourceforge.perforce.core.syncinfo.FolderSyncInfo;
import net.sourceforge.perforce.core.syncinfo.ResourceSyncInfo;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.PluginVersionIdentifier;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.team.core.RepositoryProvider;

import java.util.List;
import java.util.Map;

/**
 * The <code>PerforceDecorationRunnable</code> class is the real worker of the
 * Perforce decorator. It performs the decorations of labels and images and
 * informs the decorator whenever thos decorations are updated.
 * 
 * @version $Revision: 488 $
 * @author <a href="mailto:bpruessmann@users.sourceforge.net">Boris
 *         Pruessmann</a>
 */
class PerforceDecorationRunnable implements Runnable, IPropertyChangeListener {
  // ---------------------------------------------------------------------------
  // Private Constants

  /** Number for decorate operations that should be bacthed. */
  private static final int NUM_TO_BATCH = 50;

  // ---------------------------------------------------------------------------
  // Private Variables

  /** Indicates whether we run in Eclipse 2.1 or not. */
  private final boolean eclipse2_1;

  /** Stores a reference to the decoration notifier. */
  private IDecorationNotifier notifier;

  /** Indicates whether the 'has remote' decoration will be performed. */
  private boolean indicateHasRemote;

  /** Indicates whether the 'no remote' decoration will be performed. */
  private boolean indicateNewResource;

  /** Indicates whether the 'opened by other' decoration will be performed. */
  private boolean indicateOpenedByOther;

  /** Indicates whether the 'opened for add' decoration will be performed. */
  private boolean indicateOpenedForAdd;

  /** Indicates whether the 'opened for edit' decoration will be performed. */
  private boolean indicateOpenedForEdit;

  // -------------------------------------------------------------------------
  // Public Constructors

  /**
   * Constructor for PerforceDecorationRunnable.
   * 
   * @param notifier the object to notify.
   */
  public PerforceDecorationRunnable(IDecorationNotifier notifier) {
    this.notifier = notifier;

    IPreferenceStore store = PerforceUIPlugin.getPlugin().getPreferenceStore();
    store.addPropertyChangeListener(this);

    indicateHasRemote = store.getBoolean(IPerforceUIConstants.PREF_INDICATE_HAS_REMOTE);
    indicateNewResource = store.getBoolean(IPerforceUIConstants.PREF_INDICATE_NEW_RESOURCE);
    indicateOpenedByOther = store.getBoolean(IPerforceUIConstants.PREF_INDICATE_OPENED_BY_OTHER);
    indicateOpenedForAdd = store.getBoolean(IPerforceUIConstants.PREF_INDICATE_OPENED_FOR_ADD);
    indicateOpenedForEdit = store.getBoolean(IPerforceUIConstants.PREF_INDICATE_OPENED_FOR_EDIT);


    final PluginVersionIdentifier version = new PluginVersionIdentifier(2, 1, 0);
    final PluginVersionIdentifier resourcesVersion =
        ResourcesPlugin.getPlugin().getDescriptor().getVersionIdentifier();
    if (resourcesVersion.isCompatibleWith(version)) // Eclipse >= 2.1
    {
      eclipse2_1 = true;
    } else {
      eclipse2_1 = false;
    }
  }

  //----------------------------------------------------------------------------
  // -- Public Methods

  /** @see Runnable#run() */
  public void run() {
    List<IResource> resources = Lists.newArrayList();
    List<PerforceDecoration> decorations = Lists.newArrayList();

    while (true) {
      try {
        IResource resource = notifier.next();
        if (resource == null) {
          return;
        }

        if (eclipse2_1) {
          if (!resource.getParent().isLinked() && resource.isLinked()) {
            continue;
          }
        }

        PerforceDecoration decoration = decorate(resource);
        if (decoration != null) {
          resources.add(resource);
          decorations.add(decoration);
        }

        if (!resources.isEmpty() && (notifier.remaining() == 0 || resources.size() >= NUM_TO_BATCH)) {
          notifier.decorated(resources.toArray(new IResource[resources.size()]), decorations
              .toArray(new PerforceDecoration[decorations.size()]));
          resources.clear();
          decorations.clear();
        }
      } catch (Throwable t) {
        PerforceUIPlugin.logError("Unexpected exception.", t);
      }
    }
  }

  /** @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(PropertyChangeEvent) */
  public void propertyChange(PropertyChangeEvent event) {
    String property = event.getProperty();
    if (property.equals(IPerforceUIConstants.PREF_INDICATE_HAS_REMOTE)) {
      indicateHasRemote = ((Boolean) event.getNewValue()).booleanValue();
    } else if (property.equals(IPerforceUIConstants.PREF_INDICATE_NEW_RESOURCE)) {
      indicateNewResource = ((Boolean) event.getNewValue()).booleanValue();
    } else if (property.equals(IPerforceUIConstants.PREF_INDICATE_OPENED_BY_OTHER)) {
      indicateOpenedByOther = ((Boolean) event.getNewValue()).booleanValue();
    } else if (property.equals(IPerforceUIConstants.PREF_INDICATE_OPENED_FOR_ADD)) {
      indicateOpenedForAdd = ((Boolean) event.getNewValue()).booleanValue();
    } else if (property.equals(IPerforceUIConstants.PREF_INDICATE_OPENED_FOR_EDIT)) {
      indicateOpenedForEdit = ((Boolean) event.getNewValue()).booleanValue();
    }
  }

  //----------------------------------------------------------------------------
  // - Private Methods

  /**
   * Decorates the given resource.
   * 
   * @param resource the resource.
   * @return the decoration.
   */
  private PerforceDecoration decorate(IResource resource) {
    RepositoryProvider provider = RepositoryProvider.getProvider(resource.getProject());
    if (!resource.exists() || provider == null
        || !provider.getID().equals(PerforceCorePlugin.getTypeId())) {
      return null;
    }

    P4TeamProvider teamProvider = (P4TeamProvider) provider;
    if (teamProvider.isIgnored(resource)) {
      return new PerforceDecoration();
    }

    IPreferenceStore store = PerforceUIPlugin.getPlugin().getPreferenceStore();
    boolean isDirty = false;
    boolean computeDeepDirtyCheck = store.getBoolean(PerforceUIPlugin.CALCULATE_DIRTY_PREFERENCE);
    if (resource.getType() == IResource.FILE || computeDeepDirtyCheck) {
      isDirty = isDirty(resource);
    }

    PerforceDecoration decoration = computeDecoratedText(isDirty, resource);
    computeOverlays(isDirty, resource, decoration);

    return decoration;
  }

  /**
   * Computes the text decorations.
   * 
   * @param isDirty forced dirty flag.
   * @param resource the resource.
   * @return the PerforceDecoratin object.
   */
  private PerforceDecoration computeDecoratedText(boolean isDirty, IResource resource) {
    IPreferenceStore store = PerforceUIPlugin.getPlugin().getPreferenceStore();
    Map<String, String> bindings = Maps.newHashMapWithExpectedSize(2);

    if (isDirty) {
      bindings.put(P4DecoratorConfiguration.DIRTY_FLAG, store
          .getString(IPerforceUIConstants.PREF_DIRTY_FLAG));
    }

    if (resource.getType() != IResource.FILE) {
      LocalFolder folder = (LocalFolder) LocalResource.createFromResource(resource);
      if (folder == null) {
        return new PerforceDecoration();
      }

      FolderSyncInfo syncInfo = folder.getFolderSync();
      if (syncInfo != null && syncInfo.isAdded()) {
        bindings.put(P4DecoratorConfiguration.DIRTY_FLAG, store
            .getString(IPerforceUIConstants.PREF_ADDED_FLAG));
      }

      if (resource.getType() == IResource.PROJECT) {
        IPerforceServer server = folder.getServer();

        bindings.put(P4DecoratorConfiguration.P4PORT, server.getPort());
        bindings.put(P4DecoratorConfiguration.P4CLIENT, server.getClient());
        bindings.put(P4DecoratorConfiguration.P4USER, server.getUser());
      }
    } else {
      LocalFile localFile = (LocalFile) LocalResource.createFromResource(resource);
      if (localFile == null) {
        return new PerforceDecoration();
      }

      ResourceSyncInfo resourceSyncInfo = localFile.getSyncInfo();
      if (!resourceSyncInfo.isNull()) {
        if (resourceSyncInfo.isDeleted()) {
          return new PerforceDecoration();
        } else if (resourceSyncInfo.isAdded() || resourceSyncInfo.isBranched()) {
          bindings.put(P4DecoratorConfiguration.DIRTY_FLAG, store
              .getString(IPerforceUIConstants.PREF_ADDED_FLAG));
        } else {
          bindings.put(P4DecoratorConfiguration.HAVE_REV, "#" + resourceSyncInfo.getHaveRev());

          bindings.put(P4DecoratorConfiguration.HEAD_REV, "#" + resourceSyncInfo.getHeadRev());
        }
      }
    }

    return new PerforceDecoration();
  }

  /**
   * Computes the graphical decorations.
   * 
   * @param isDirty forced dirty flag.
   * @param resource the resource.
   * @param decoration the decoration object.
   */
  private void computeOverlays(boolean isDirty, IResource resource, PerforceDecoration decoration) {
    List<ImageDescriptor> overlays = Lists.newArrayList();
    List<Integer> locations = Lists.newArrayList();

    switch (resource.getType()) {
      case IResource.PROJECT:
        overlays.add(OverlayIconCache.HAS_REMOTE);
        locations.add(Integer.valueOf(OverlayIconCache.HAS_REMOTE_LOCATION));
        break;

      case IResource.FOLDER: {
        LocalResource localResource = LocalResource.createFromResource(resource);
        if (localResource == null) {
          return;
        }

        if (localResource.hasRemote() && indicateHasRemote) {
          overlays.add(OverlayIconCache.HAS_REMOTE);
          locations.add(Integer.valueOf(OverlayIconCache.HAS_REMOTE_LOCATION));
        }
      }
        break;

      case IResource.FILE: {
        LocalFile localFile = (LocalFile) LocalResource.createFromResource(resource);
        if (localFile == null) {
          return;
        }

        if (!localFile.hasRemote() && indicateNewResource) {
          overlays.add(OverlayIconCache.NO_REMOTE);
          locations.add(Integer.valueOf(OverlayIconCache.NO_REMOTE_LOCATION));
        } else if (localFile.hasRemote()) {
          if (localFile.isOutOfDate()) {
            overlays.add(OverlayIconCache.OUT_OF_DATE);
            locations.add(Integer.valueOf(OverlayIconCache.OUT_OF_DATE_LOCATION));
          } else if (indicateHasRemote) {
            overlays.add(OverlayIconCache.HAS_REMOTE);
            locations.add(new Integer(OverlayIconCache.HAS_REMOTE_LOCATION));
          }
        }

        ResourceSyncInfo resourceSyncInfo = localFile.getSyncInfo();
        if (!resourceSyncInfo.isNull()) {
          if (resourceSyncInfo.isOtherOpen() && indicateOpenedByOther) {
            overlays.add(OverlayIconCache.OTHER_OPEN_FOR_EDIT);
            locations.add(Integer.valueOf(OverlayIconCache.OTHER_OPEN_FOR_EDIT_LOCATION));
          }

          if (resourceSyncInfo.hasOurLock()) {
            overlays.add(OverlayIconCache.OUR_LOCK);
            locations.add(Integer.valueOf(OverlayIconCache.OUR_LOCK_LOCATION));
          }

          if (resourceSyncInfo.isDeleted()) {
            overlays = Lists.newArrayList();
            locations = Lists.newArrayList();
          } else if (indicateOpenedForAdd
              && (resourceSyncInfo.isAdded() || resourceSyncInfo.isBranched())) {
            overlays.add(OverlayIconCache.OPEN_FOR_ADD);
            locations.add(Integer.valueOf(OverlayIconCache.OPEN_FOR_ADD_LOCATION));
          } else if (indicateOpenedForEdit && resourceSyncInfo.isEdited()) {
            overlays.add(OverlayIconCache.OPEN_FOR_EDIT);
            locations.add(Integer.valueOf(OverlayIconCache.OPEN_FOR_EDIT_LOCATION));
          }
        }
      }
        break;
    }

    if (!overlays.isEmpty()) {
      Integer[] integers = locations.toArray(new Integer[locations.size()]);
      int[] ints = new int[integers.length];
      for (int i = 0; i < integers.length; i++) {
        ints[i] = integers[i].intValue();
      }
      decoration.setLocations(ints);
      decoration.setOverlays(overlays);
    }
  }

  /**
   * Calucaltes the dirty state of the given resource.
   * 
   * @param resource the resource.
   * @return true if resource is dirty, false otherwise.
   */
  private boolean isDirty(IResource resource) {
    final CoreException decoratorException =
        new CoreException(new Status(IStatus.OK, "id", 1, "", null));//$NON-NLS-1$ //$NON-NLS-2$
    try {
      resource.accept(new IResourceVisitor() {
        public boolean visit(IResource resource) throws CoreException {

          // a project can't be dirty, continue with its children
          if (resource.getType() == IResource.PROJECT) {
            return true;
          }

          // if the resource does not exist in the workbench or on the
          // file system, stop searching.
          if (!resource.exists()) {
            return false;
          }

          LocalResource localResource = LocalResource.createFromResource(resource);
          if (localResource.isIgnored()) {
            return false;
          }

          if (localResource instanceof LocalFile) {
            if (localResource.isDirty()) {
              throw decoratorException;
            }
          }

          return true;
        }
      }, IResource.DEPTH_INFINITE, true);
    } catch (CoreException e) {
      // if our exception was caught, we know there's a dirty child
      return e == decoratorException;
    }

    return false;
  }
}
